package com.breeze.hib;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Junction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.Subqueries;
import org.hibernate.internal.CriteriaImpl;
import org.hibernate.internal.CriteriaImpl.OrderEntry;
import org.hibernate.transform.Transformers;
import com.breeze.metadata.IDataProperty;
import com.breeze.metadata.IEntityType;
import com.breeze.metadata.INavigationProperty;
import com.breeze.metadata.MetadataHelper;
import com.breeze.query.AndOrPredicate;
import com.breeze.query.AnyAllPredicate;
import com.breeze.query.BinaryPredicate;
import com.breeze.query.EntityQuery;
import com.breeze.query.Expression;
import com.breeze.query.LitExpression;
import com.breeze.query.Operator;
import com.breeze.query.OrderByClause;
import com.breeze.query.OrderByClause.OrderByItem;
import com.breeze.query.Predicate;
import com.breeze.query.PropExpression;
import com.breeze.query.SelectClause;
import com.breeze.query.UnaryPredicate;
/**
* Converts EntityQuery into Hibernate Criteria.
*
* @author Jay
* @see http://docs.jboss.org/hibernate/core/3.6/javadocs/org/hibernate/Criteria.html
*/
/**
* @author Jay
*
*/
class CriteriaBuilder {
// TODO: fix select (projection) with nonscalar nav props. So far no idea how to do this...
private CriteriaAliasBuilder _aliasBuilder;
@SuppressWarnings("unused")
private EntityQuery _entityQuery;
private IEntityType _entityType;
private int _subqCount = 0;
private CriteriaBuilder() {
}
public static CriteriaBuilder create(Criteria crit, IEntityType entityType, EntityQuery entityQuery) {
CriteriaBuilder critBuilder = new CriteriaBuilder();
critBuilder.updateCriteria(crit, entityType, entityQuery);
return critBuilder;
}
/**
* @param crit
* a Criteria object that will be updated to match the entityQuery
* @param entityQuery
* the EntityQuery object from which the criteria should be updated.
*/
private void updateCriteria(Criteria crit, IEntityType entityType, EntityQuery entityQuery) {
_entityType = entityType;
_entityQuery = entityQuery;
_aliasBuilder = new CriteriaAliasBuilder();
Integer takeCount = entityQuery.getTakeCount();
if (takeCount != null && takeCount == 0) {
// Hack because setMaxResults(0) returns all records instead of
// none.
// so we do then equiv of skip 'everything' instead.
// and then insure that we don't overwrite this with another skip.
crit.setFirstResult(Integer.MAX_VALUE);
} else {
if (takeCount != null) {
crit.setMaxResults(takeCount);
}
Integer skipCount = entityQuery.getSkipCount();
if (skipCount != null) {
crit.setFirstResult(skipCount);
}
}
addWhere(crit, entityQuery.getWherePredicate());
addSelect(crit, entityQuery.getSelectClause());
addOrderBy(crit, entityQuery.getOrderByClause());
}
/**
* Apply the OData $inlinecount to the (already filtered) Criteria. Removes
* $skip and $top and $orderby operations and adds a rowCount projection.
*
* @param crit
* a Criteria object. Should already contain only filters that
* affect the row count.
* @return the same Criteria that was passed in, with operations added.
*/
public Criteria applyInlineCount(Criteria crit) {
crit.setMaxResults(0);
crit.setFirstResult(0);
CriteriaImpl impl = (CriteriaImpl) crit;
Iterator<OrderEntry> iter = impl.iterateOrderings();
while (iter.hasNext()) {
iter.next();
iter.remove();
}
crit.setProjection(Projections.rowCount());
return crit;
}
/**
* @return whether this
*/
public boolean containsNavPropertyProxy() {
return _aliasBuilder.containsNavPropertyProxy();
}
private void addWhere(Criteria crit, Predicate wherePred) {
if (wherePred == null) return;
CriteriaWrapper critWrapper = new CriteriaWrapper(crit, _entityType);
Criterion cr = toCriterion(critWrapper, wherePred, null);
crit.add(cr);
}
private void addSelect(Criteria crit, SelectClause selectClause) {
if (selectClause == null) return;
ProjectionList projList = Projections.projectionList();
CriteriaWrapper critWrapper = new CriteriaWrapper(crit, _entityType);
for (String propertyPath : selectClause.getPropertyPaths()) {
String propertyName = _aliasBuilder.getPropertyName(critWrapper,
propertyPath);
projList.add(Projections.property(propertyName).as(propertyPath));
}
crit.setProjection(projList);
crit.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
}
// Basically doing something like this for nested props
/*
* Criteria criteria = session.createCriteria(OrderDetail.class)
* .createAlias("product", "product_1") .createAlias("product_1.category",
* "category_2") .addOrder( Order.desc(category_2.name");
*/
private void addOrderBy(Criteria crit, OrderByClause obc) {
if (obc == null) return;
CriteriaWrapper critWrapper = new CriteriaWrapper(crit, _entityType);
for (OrderByItem item : obc.getOrderByItems()) {
String propertyName = _aliasBuilder.getPropertyName(critWrapper,
item.getPropertyPath());
Order order = item.isDesc() ? Order.desc(propertyName) : Order
.asc(propertyName);
crit.addOrder(order);
}
}
// crit is cast as Object because it can be either a Criteria or a DetachedCriteria
// and these two classes do not share any useful interfaces.
private Criterion toCriterion(CriteriaWrapper crit, Predicate pred,
String contextAlias) {
if (pred instanceof AndOrPredicate) {
return createCriterion(crit, (AndOrPredicate) pred, contextAlias);
} else if (pred instanceof AnyAllPredicate) {
return createCriterion(crit, (AnyAllPredicate) pred, contextAlias);
} else if (pred instanceof BinaryPredicate) {
return createCriterion(crit, (BinaryPredicate) pred, contextAlias);
} else if (pred instanceof UnaryPredicate) {
return createCriterion(crit, (UnaryPredicate) pred, contextAlias);
} else {
throw new RuntimeException("Unable to recognize predicate: "
+ pred.getOperator().getName());
}
}
// { resourceName: 'Orders', where: or: [{ 'employee.lastName': 'smith }, {
// freight: 100 }] }
// createCriteria(Order.class)
// .createAlias('employee', 'e')
// .add(disjunction()
// .add(Restrictions.eq(e.lastName, 'smith')
// .add(Restrictions.eq(freight, 100);
private Criterion createCriterion(CriteriaWrapper crit, AndOrPredicate pred,
String contextAlias) {
Operator op = pred.getOperator();
Junction junction = (op == Operator.And) ? Restrictions.conjunction()
: Restrictions.disjunction();
for (Predicate subPred : pred.getPredicates()) {
Criterion cr = toCriterion(crit, subPred, contextAlias);
junction.add(cr);
}
return junction;
}
private Criterion createCriterion(CriteriaWrapper crit, AnyAllPredicate pred,
String contextAlias) {
Operator op = pred.getOperator();
if (op == Operator.Any) {
DetachedCriteria detCrit = makeSubcrit(crit, pred);
Criterion cr = Subqueries.exists(detCrit);
return cr;
} else if (op == Operator.All) {
DetachedCriteria detCrit = makeSubcrit(crit, pred);
Criterion cr = Subqueries.notExists(detCrit);
return cr;
}
// OLD logic using joins - cannot be 'inverted' to make an 'any' into an 'all though
// if (op == Operator.Any) {
// PropExpression pexpr = pred.getExpr();
// Predicate nextPred = pred.getPredicate();
// // TODO: should check that propertyPath below is not nested - i.e. a
// // simple navigation propertyName
// String propertyPath = pexpr.getPropertyPath();
// String nextContextAlias = _aliasBuilder.getSimpleAlias(crit, propertyPath);
// Criterion cr = toCriterion(crit, nextPred, nextContextAlias);
// return cr;
// }
throw new RuntimeException("'All' predicates are not yet supported.");
// Criteria criteria = session.createCriteria(CD.class, "cd");
// criteria.createAlias("cd.tracks", "track");
// criteria.add(Restrictions.eq("track.title", "someTitle"));
// IF USING exists then we need join columns
// Criteria criteria = session.createCriteria(CD.class, "cd");
// DetachedCriteria trackCriteria = DetachedCriteria.forClass(Track.class, "track");
// trackCriteria.add(Restrictions.eq("track.title", "SomeTitle"));
// trackCriteria.add(Restrictions.propertyEq("track.cd_id", "cd.id"));
// trackCriteria.setProjection(Projections.property("track.id"));
// criteria.add(Subqueries.exists(trackCriteria));
}
private DetachedCriteria makeSubcrit(CriteriaWrapper crit, AnyAllPredicate pred) {
PropExpression pexpr = pred.getExpr();
Predicate nextPred = pred.getPredicate();
if (pred.getOperator() == Operator.All) {
nextPred = new UnaryPredicate(Operator.Not, nextPred);
}
// TODO: should check that propertyPath below is not nested -
// i.e. should be a simple navigation propertyName
IEntityType parentType = pexpr.getEntityType();
List<IDataProperty> rootKeyProperties = parentType.getKeyProperties();
String propertyPath = pexpr.getPropertyPath();
INavigationProperty navProp = (INavigationProperty) MetadataHelper.getPropertyFromPath(propertyPath, parentType);
String[] subtypeFkNames = navProp.getInvForeignKeyNames();
IEntityType subtype = navProp.getEntityType();
List<IDataProperty> subtypeKeyProperties = subtype.getKeyProperties();
Class subqueryClass = MetadataHelper.lookupClass(subtype.getName());
String subqAlias = "subq" + _subqCount++;
DetachedCriteria detCrit = DetachedCriteria.forClass(subqueryClass, subqAlias);
CriteriaWrapper detWrapper = new CriteriaWrapper(detCrit, subtype);
Criterion subCrit = toCriterion(detWrapper, nextPred, null);
detCrit.add(subCrit);
Criterion joinCrit = new PropertyExpression(
subqAlias + "." + subtypeFkNames[0],
crit.getAlias() + "." + rootKeyProperties.get(0).getName(),
"=");
detCrit.add(joinCrit);
detCrit.setProjection(Projections.property(subqAlias + "." + subtypeKeyProperties.get(0).getName()));
return detCrit;
}
private Criterion createCriterion(CriteriaWrapper crit, UnaryPredicate pred,
String contextAlias) {
Criterion cr = toCriterion(crit, pred.getPredicate(), contextAlias);
return Restrictions.not(cr);
}
private Criterion createCriterion(CriteriaWrapper crit, BinaryPredicate pred,
String contextAlias) {
Operator op = pred.getOperator();
String symbol = _operatorMap.get(op.getName());
Expression expr1 = pred.getExpr1();
Expression expr2 = pred.getExpr2();
Criterion cr;
if (expr1 instanceof PropExpression) {
PropExpression pexpr1 = (PropExpression) expr1;
String propPath = pexpr1.getPropertyPath();
String propName;
if (pexpr1.getProperty().getParentType().isComplexType()) {
// don't process the property path in this case.
propName = propPath;
} else {
propName = _aliasBuilder.getPropertyName(crit, propPath);
}
propName = (contextAlias == null) ? propName : contextAlias + "."
+ propName;
if (expr2 instanceof LitExpression) {
Object value = ((LitExpression) expr2).getValue();
if (value == null) {
if (op == Operator.Equals) {
cr = Restrictions.isNull(propName);
} else if (op == Operator.NotEquals) {
cr = Restrictions.isNotNull(propName);
} else {
throw new RuntimeException("Binary Predicate with a null value and the "
+ op.getName() + "operator is not supported .");
}
} else if (symbol != null) {
cr = new OperatorExpression(propName, value, symbol);
} else if (op == Operator.In) {
cr = Restrictions.in(propName, ((List) value).toArray());
} else if (op == Operator.StartsWith) {
cr = Restrictions.like(propName, ((String) value),
MatchMode.START);
} else if (op == Operator.EndsWith) {
cr = Restrictions.like(propName, (String) value,
MatchMode.END);
} else if (op == Operator.Contains) {
cr = Restrictions.like(propName, ((String) value),
MatchMode.ANYWHERE);
} else {
throw new RuntimeException("Binary Predicate with the "
+ op.getName() + "operator is not yet supported.");
}
} else {
// javax.persistence.criteria.CriteriaBuilder x = new
// javax.persistence.criteria.CriteriaBuilder();
String otherPropPath = ((PropExpression) expr2)
.getPropertyPath();
if (symbol != null) {
cr = new PropertyExpression(propName, otherPropPath, symbol);
} else if (op == Operator.StartsWith) {
cr = new LikePropertyExpression(propName, otherPropPath,
MatchMode.START);
} else if (op == Operator.EndsWith) {
cr = new LikePropertyExpression(propName, otherPropPath,
MatchMode.END);
} else if (op == Operator.Contains) {
cr = new LikePropertyExpression(propName, otherPropPath,
MatchMode.ANYWHERE);
} else {
throw new RuntimeException("Property comparison with the "
+ op.getName() + "operator is not yet supported.");
}
}
return cr;
} else {
throw new RuntimeException(
"Function expressions not yet supported.");
}
}
private static final HashMap<String, String> _operatorMap = new HashMap<String, String>();
static {
_operatorMap.put("eq", "=");
_operatorMap.put("ne", "<>");
_operatorMap.put("gt", ">");
_operatorMap.put("ge", ">=");
_operatorMap.put("lt", "<");
_operatorMap.put("le", "<=");
}
}